﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

using System.IO.Ports;

namespace UsbBitWacker
{
    public class UBW : IDisposable
    {
        internal bool _isInitialized = false;
        internal SerialPort _port = null;

        public const string PORTA = "A";
        public const string PORTB = "B";
        public const string PORTC = "C";
        public string[] Ports = { PORTA, PORTB, PORTC };

        public string ErrorString;
        public string GetErrorString()
        {
            return ErrorString;
        }
        internal void resetErrorString()
        {
            ErrorString = "";
        }

        public static string FindUbwComPort()
        {
            string portName;
            string[] portsAvailable = SerialPort.GetPortNames();
            var ports = SerialPortEnumator.BuildPortNameHash(portsAvailable);
            var ubwPorts = (from p in ports where p.Value.Contains("UBW") select p);
            if (ubwPorts.Count() > 0)
            {
                var ubwEntry = ubwPorts.First();
                portName = ubwEntry.Key;
            }
            else
            {
                throw new ApplicationException("UBW port not found");
            }

            return portName;
        }

        public string GetConnectedPort()
        {
            if (_port != null)
                return _port.PortName;
            else
                return "";
        }

        public int IsInitialized()
        {
            return _isInitialized ? 1 : 0;
        }

        public int Open()
        {
            resetErrorString();

            try
            {
                string ubwComPort = UBW.FindUbwComPort();
                return Open(ubwComPort);
            }
            catch (Exception ex)
            {
                ErrorString = String.Format("Failed to open com port: {0}: {1}", ex.GetType().Name, ex.Message);
                Console.WriteLine(ErrorString);
            }

            return 0;
        }
        public int Open(string port)
        {
            bool success = true;
            resetErrorString();

            try
            {
                if (!string.IsNullOrEmpty(port))
                {
                    _port = new SerialPort(port, 115200, Parity.None, 8, StopBits.One);
                    _port.ReadTimeout = 500;
                    _port.WriteTimeout = 500;
                    _port.Open();

                    // Verify the device is a UBW
                    _port.WriteLine("V");
                    string response = _port.ReadLine();
                    if (!response.Contains("UBW"))
                    {
                        success = false;
                        ErrorString = "Device on " + port +" is not a UBW. Version response is: " + response;
                        this.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                ErrorString = String.Format("Failed to open com port {0}: {1}: {2}", port, ex.GetType().Name, ex.Message);
                Console.WriteLine(ErrorString);
                
                success = false;
            }

            _isInitialized = success;
            return success ? 1 : 0;
        }

        public int Close()
        {
            if (_port != null)
            {
                try
                {
                    _port.Close();
                    _port.Dispose();
                }
                catch (ObjectDisposedException)
                {
                }
                finally
                {
                    _port = null;
                    _isInitialized = false;
                }
            }

            return 1;
        }

        private object _portLock = new object();
        public string Query(string command)
        {
            string response = "";

            lock (_portLock)
            {
                if (_port.BytesToRead > 0)
                    _port.DiscardInBuffer();

                // Send command
                _port.WriteLine(command + "\r\n");
                System.Diagnostics.Debug.Print("UBWCMD: {0}", command);

                // Receive response
                response = _port.ReadTo("\r\n");

            }
            return response;
        }

        public bool SendCommand(string command)
        {
            resetErrorString();

            string response = Query(command);

            if (response.Equals("OK"))
                return true;
            else
            {
                Console.WriteLine(response);
                ErrorString = "Invalid response: " + response;
                return false;
            }
        }

        #region Commands

        /// <summary>
        /// Outputs a square wave at a particular frequency on any pin. Only one pin may be used as a frequency output at a time.
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <param name="frequencyHz">Frequency in Hz (1 to 30000)</param>
        /// <returns>True if succeeded, otherwise False.</returns>
        /// <remarks>
        /// This command will automatically set the specified pin to be an output. If the pin was previously an analog input,
        /// you must set it to digital mode in order for the F command to work properly.
        /// </remarks>
        public bool SetFrequencyOutput(string port, int pin, int frequencyHz)
        {
            string cmd = String.Format("F,{0},{1},{2}", frequencyHz, port, pin);
            return SendCommand(cmd);
        }

        /// <summary>
        /// Outputs a square wave at a particular frequency on any pin. Only one pin may be used as a frequency output at a time.
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <param name="frequencyHz">Frequency in Hz (1 to 30000)</param>
        /// <param name="duty">The duty cycle of the square wave (1 to 99)</param>
        /// <returns>True if succeeded, otherwise False.</returns>
        /// <remarks>
        /// This command will automatically set the specified pin to be an output. If the pin was previously an analog input,
        /// you must set it to digital mode in order for the F command to work properly.
        /// </remarks>
        public bool SetFrequencyOutput(string port, int pin, int frequencyHz, int duty)
        {
            string cmd = String.Format("F,{0},{1},{2},{3}", frequencyHz, port, pin, frequencyHz, duty);
            return SendCommand(cmd);
        }

        /// <summary>
        /// Configures the IO pin directions for ports A, B, and C, as well as enable analog inputs. Input = 1, Output = 0.
        /// </summary>
        /// <param name="dirMaskA">8 bit mask for port A pins (0-255)</param>
        /// <param name="dirMaskB">8 bit mask for port B pins (0-255)</param>
        /// <param name="dirMaskC">8 bit mask for port C pins (0-255)</param>
        /// <param name="analogEnableCount">The number of analog channels to enable (1 to 13)</param>
        /// <returns>True if succeeded, otherwise False.</returns>
        /// <remarks>
        /// Warning on Analog Inputs:
        /// It is very important that if you enable an analog input on a pin, that you set that pin as an input (set the proper dirMaskX bit). If you have a pin set as
        /// an output in the dirMaskX bit but have it enabled as an analog input, your analog reading will simply convert the current digital output voltage on the pin
        /// (which may be what you want, but probably not). Also, if you have a pin set as a digital output with the dirMaskX bit, but DON'T enable it as an analog input
        /// and then apply analog input levels to the pin, that pin may draw excessive power because it may float around between a High (5V) and Low (0V). PICs are
        /// very robust, but be careful.
        /// 
        /// Analog Input Matrix:
        /// If one or more of the analog inputs are enabled, then use this chart to see which number for ananlogEnableCount enables which pins as analog inputs. The PIC
        /// core that all of the USB PICs are built on has 13 analog input channels. However, on the 28 pin parts (which are currently used in UBW designs) three of those
        /// analog inputs (AN5,AN6 and AN7) do not come out as pins. They still exist, and as far as Firmware D v1.4.4 is concerned, are treated exactly as all of the
        /// other analog input pins, so the show up in the "A" packet.  What this means is that you can get up to 10 separate analog inputs on a UBW.
        /// 
        /// 0  = None
        /// 1  = RA0
        /// 2  = RA1
        /// 3  = RA2
        /// 4  = RA3
        /// 5  = RA5
        /// 6  = N/A
        /// 7  = N/A
        /// 8  = N/A
        /// 9  = RB2
        /// 10 = RB3
        /// 11 = RB1
        /// 12 = RB4
        /// 13 = RB0
        /// 
        /// </remarks>
        public bool ConfigureIO(int dirMaskA, int dirMaskB, int dirMaskC, int analogEnableCount)
        {
            string cmd = String.Join(",", "C", dirMaskA, dirMaskB, dirMaskC, analogEnableCount);
            return SendCommand(cmd);
        }

        /// <summary>
        /// Writes the Port A, B, and C outputs all at once.
        /// </summary>
        /// <param name="portA">Port A value (0-255)</param>
        /// <param name="portB">Port B value (0-255)</param>
        /// <param name="portC">Port C value (0-255)</param>
        /// <returns></returns>
        public bool WriteAllOutputs(int portA, int portB, int portC)
        {
            string cmd = String.Join(",", "O", portA, portB, portC);
            return SendCommand(cmd);
        }
        
        /// <summary>
        /// Reads the digital inputs on all of the ports and return a comma separated string with the values.
        /// </summary>
        /// <returns>The values read on each port in the format A,B,C if succeed, otherwise an error string start with a "!"</returns>
        public string ReadAllInputsCSV()
        {
            string result = Query("I");
            if (result.StartsWith("I,"))
                result = result.Substring(2);

            return result;
        }

        public int[] ReadAllInputs()
        {
            string[] portValuesCsv = ReadAllInputsCSV().Split(',');
            int portA, portB, portC;

            int.TryParse(portValuesCsv[0], out portA);
            int.TryParse(portValuesCsv[1], out portB);
            int.TryParse(portValuesCsv[2], out portC);

            return new int[] { portA, portB, portC };
        }

        /// <summary>
        /// Returns the version string exactly as it is returned by the V command
        /// </summary>
        /// <returns>The version string</returns>
        public string GetVersion()
        {
            return Query("V");
        }


        /// <summary>
        /// Resets the UBW to its default state. It will initialize all pins to digital inputs and stop any running timers.
        /// </summary>
        public void Reset()
        {
            SendCommand("R");
        }

        // NOTE: This is commented out because there is no handling of async data from the UBW
        //public void MonitorDigitalInputs()
        //{
        //    // T,TimeInterval_ms,0
        //}

        // NOTE: This is commented out because there is no handling of async data from the UBW
        //public void MonitorAnalogInputs()
        //{
        //    // T,TimeInterval_ms,1
        //}

        /// <summary>
        /// Sample the analog inputs and return the results in a string in the format AN0, AN1, AN2, AN3, etc.
        /// Note that AN5, AN6 and AN7 will be included in the returned string but are not accessible as pins so you'll need to ignore them.
        /// </summary>
        /// <remarks>
        /// The results in a string in the format AN0, AN1, AN2, AN3, etc
        /// </remarks>
        public string ReadAnalogInputsCSV()
        {
            string result = Query("A");
            if (result.StartsWith("A,"))
                result = result.Substring(2);

            return result;
        }

        /// <summary>
        /// Sample the analog inputs and return the results in a string in the format AN0, AN1, AN2, AN3, etc.
        /// Note that AN5, AN6 and AN7 will NOT be included in the returned array.
        /// </summary>
        /// <returns>
        /// Array of ints read from the enabled analog inputs. AN5 to AN7 are NOT included, so array position 5 = AN8, position 6 = AN9, etc;
        /// </returns>
        public int[] ReadAnalogInputs()
        {
            string analogCsv = ReadAnalogInputsCSV();
            string[] strValues = analogCsv.Split(',');
            List<int> values = new List<int>();
            int value = -1;

            for (int i = 0; i < strValues.Length; i++)
            {
                if ((i >= 5) && (i <= 7)) break;
                value = -1;
                int.TryParse(strValues[i], out value);
                values.Add(value);
            }

            return values.ToArray();
        }

        /// <summary>
        /// Sample the analog inputs and return the result of a single channel. Note that all inputs are sampled, but only the value of the specified channel is returned.
        /// </summary>
        /// <param name="channel">The channel to get the value of (0 to 12)</param>
        /// <returns>The analog value for the channel (0-1023). If the channel is invalid, a -1 is returned.</returns>
        public int ReadAnalogInput(int channel)
        {
            string result = ReadAnalogInputsCSV();
            string[] strValues = result.Split(',');
            int value = -1;

            if ((channel >= 0) && (channel < strValues.Length))
            {
                int.TryParse(strValues[channel], out value);
            }

            return value;
        }

        //public int MemoryRead(int address)
        //{
        //    //MR
        //}

        //public bool MemoryWrite(int address, int value)
        //{
        //    //MW
        //}

        /// <summary>
        /// Configure an individual pin as an input
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <returns></returns>
        public bool ConfigurePinAsInput(string port, int pin)
        {
            return ConfigurePinDirection(port, pin, asInput: true);
        }

        /// <summary>
        /// Configure an individual pin as an output
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <returns></returns>
        public bool ConfigurePinAsOutput(string port, int pin)
        {
            return ConfigurePinDirection(port, pin, asInput: false);
        }

        /// <summary>
        /// Configure the data direction (input or output) of an individual pin
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <param name="asInput">True = input, False = output</param>
        /// <returns></returns>
        public bool ConfigurePinDirection(string port, int pin, bool asInput)
        {
            string cmd = String.Join(",", "PD", port, pin, asInput ? 1 : 0);
            return SendCommand(cmd);
        }

        /// <summary>
        /// Reads a pin value as a digital input.
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <returns>1 = HIGH, 0 = LOW</returns>
        public int ReadDigitalPin(string port, int pin)
        {
            string cmd = String.Join(",", "PI", port, pin);
            string response = Query(cmd);

            int result = -1;
            if (response.StartsWith("PI,"))
            {
                response = response.Substring(3);
                int.TryParse(response, out result);
            }

            return result;
        }

        /// <summary>
        /// Write a value to a digital output pin.
        /// </summary>
        /// <param name="port">Port Name (A, B, or C)</param>
        /// <param name="pin">Pin Number (0 to 7)</param>
        /// <param name="value">Pin vlaue (1=HIGH, 0=LOW)</param>
        public bool WriteDigitalPin(string port, int pin, int value)
        {
            string cmd = String.Join(",", "PO", port, pin, value);
            return SendCommand(cmd);
        }
        

        // NOTE: THESE RC COMMANDS ARE UNTESTED
        //public bool EnableRCServoOutput(string port, int pin, int pulse_width_percent)
        //{
        //    int pulseVal = (pulse_width_percent / 100) * 11890;
        //    if (pulse_width_percent == 0)
        //        pulseVal = 1;

        //    string cmd = string.Join(",", "RC", port, pin, pulseVal);
        //    return SendCommand(cmd);
        //}

        //public bool DisableRCServoOutput(string port, int pin)
        //{
        //    string cmd = String.Join(",", "RC", port, pin, 0);
        //    return SendCommand(cmd);
        //}

        // Bulk Commands???
        
        #endregion

        #region IDisposable Implementation
        
        // Dispose() calls Dispose(true)
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing) 
            {
                // free managed resources
                if (_port != null)
                {
                    _port.Dispose();
                    _port = null;
                }
            }
            // free native resources if there are any.
            //if (nativeResource != IntPtr.Zero) 
            //{
            //    Marshal.FreeHGlobal(nativeResource);
            //    nativeResource = IntPtr.Zero;
            //}
        }

        #endregion

        public static int BuildDirMask(DataDir p7, DataDir p6, DataDir p5, DataDir p4, DataDir p3, DataDir p2, DataDir p1, DataDir p0)
        {
            int val = 0;

            val |= (int)p0 << 0;
            val |= (int)p1 << 1;
            val |= (int)p2 << 2;
            val |= (int)p3 << 3;
            val |= (int)p4 << 4;
            val |= (int)p5 << 5;
            val |= (int)p6 << 6;
            val |= (int)p7 << 7;

            return val;
        }


        public static bool DecodeDigitalPinValue(int portA, int portB, int portC, string port, int pin)
        {
            int portValue = 0;

            if (port == PORTA)
                portValue = portA;
            else if (port == PORTB)
                portValue = portB;
            else if (port == PORTC)
                portValue = portC;

            int pinValue = portValue & (1 << pin);

            return (pinValue == 0) ? false : true;
        }

    }

}
